package com.uxsino.simo.collector.connections;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.zip.GZIPInputStream;

import javax.net.ssl.SSLContext;

import com.uxsino.simo.connections.target.CDPSTarget;
import lombok.Data;

import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.CookieStore;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.ssl.SSLContextBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.uxsino.commons.utils.RSA;
import com.uxsino.simo.connections.AbstractConnection;
import com.uxsino.simo.connections.exception.SimoConnectionException;
import com.uxsino.simo.connections.exception.SimoQueryException;

public class HTTPCDPSConnection extends AbstractConnection<CDPSTarget> {
    public static Logger logger = LoggerFactory.getLogger(HTTPCDPSConnection.class);

    public static final String PRIVATE_KEY = "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAJqrFR0MeyABst8OBYQUqO2iC6qCy5xFaZ0n9R4bo2dpqp2HZhtR6FprKJgRqanq9KG/cZQJM+TqRvmBk/PWxTWPWZ+KGikKiIN7mQaTlzWohmZDuqeVfKXb5/kuX8LPVT+zuH5LjFaQA5EpNFdtu+8nRc06z15p1gGkWXJ44KMrAgMBAAECgYAl357b7iJ4Q/I5DIXtQeHbzsJsjnqtWZHzGcW8CaiJuiY9TNqD9hqfiX33Ptv877vXn7mANoCunW+jyUchfoXro6sPB0FLVurWtIpXgKowrWpOiccNd9xWgT52H8IYHtC8BzDu7SoRTczNs8P3FkbPRTJdSkM8M18TSoiEpjd+gQJBAPlxBgNzRBA6IMb/oUF3+FR1O+E8oZQnTZwekzZ8FVPP1A1Nf/mZM5JKnEbcZ0oRKZTykyY8fuRDZa7w6wk55W8CQQCevCVbgx7I8ooFfeTDKfAgVaIGQ3WPV6r70K69Deu+wwOkrgXAk/VvE6Lx4t4cCylukPQzMYaYR7gi6CjDr1gFAkEAlVU/12LzdY+HPfueS7aKGe4RijelODALe5KmaXEBx6pddhVWr1QzlrpKMvYSdDowHbbXt4VyJi/fOUuMOnrJywJALZcuR51eTyrSF4e0F5XJARB3S8M+VaBBXLfwKB6CcjTCrB7LSid026VRLJYTYwyVpsr9SGLWHJCkw2nYo3OOHQJBAI52dSpVW3mHUtyyfWrQWkMWeLF5UQeHMk+M6nveMOX2TZLe3Apu1+K3A2p9ZPY9qUQEF8ZK25yZ+J2Uo5aqDHk=";

    private static int DEFAULT_TIMEOUT_MS = 100000;

    public static int KEY_SIZE = 1024;

    public static String DEFAULT_ENCODING = "UTF-8";

    private String tokenStr;

    private CloseableHttpClient client;

    private HttpClientContext context;

    private CookieStore cookieStore;

    private String targetIP;

    private String targetNeClass;

    private String urlBase;

    private RequestConfig requestConfig;

    @Override
    public int connect(CDPSTarget target) {
        super.connect(target);
        state = 0;
        connected = false;
        targetIP = target.host;
        targetNeClass = target.type;

        HttpClientBuilder builder = HttpClientBuilder.create();

        try {
            SSLContext sslContext = SSLContextBuilder.create().loadTrustMaterial(null, (chain, authType) -> true)
                .build();
            builder.setSSLContext(sslContext);
        } catch (Exception e) {
            logger.error("error setting ssl context", e);
        }
        builder.setSSLHostnameVerifier((s, sslSession) -> true);

        context = HttpClientContext.create();
        urlBase = target.iProtocol.trim().toLowerCase()+"://" + target.mgrIp + ":" + target.port;
        cookieStore = new BasicCookieStore();
        context.setCookieStore(cookieStore);
        client = builder.build();
        requestConfig = RequestConfig.custom().setSocketTimeout(DEFAULT_TIMEOUT_MS)
            .setConnectTimeout(DEFAULT_TIMEOUT_MS).setConnectionRequestTimeout(DEFAULT_TIMEOUT_MS).build();

        if (handShake(target)) {
            connected = true;
            state = 1;
        }
        return state;
    }

    @Override
    public Object execCmd(Object cmdPattern) throws SimoQueryException, SimoConnectionException {
        String cmdStr = (String) cmdPattern;
        String[] cmds = cmdStr.split("###");
        if (cmds.length != 3) {
            throw new SimoQueryException("illegal query format, cmd:" + cmdStr);
        }

        JSONObject jsonObj = JSON.parseObject(cmds[2]);
        jsonObj.put("ip", targetIP);
        jsonObj.put("osType", targetNeClass);
        jsonObj.put("token", tokenStr);

        String postRes = execPost(urlBase + cmds[0], jsonObj.toJSONString());
        JSONObject res = JSON.parseObject(postRes);
        if (!"success".equals(res.getString("rstcode"))) {
            throw new SimoQueryException("CDPS response error "+res.toString());
        }

        return res.get(cmds[1]);
    }

    @Override
    public Object buildCmd(String cmdPattern, Map<String, String> args) throws SimoQueryException {
        return cmdPattern;
    }

    @Override
    public int close() {
        connected = false;
        state = 0;
        try {
        	if(client != null) {
        		client.close();        		
        	}
        } catch (IOException e) {
            logger.error("close http client error", e);
        }
        return super.close();
    }

    @Override
    public boolean testWithConnected(String cmdString, String resStart) {
        if (!isConnected()) {
            return false;
        }

        String cmd = "/simotoken###data###{\"request\":{\"subRequest\":\"whostInfoCheck\",\"mainRequest\":\"sysinfo\",\"ssubRequest\":\"\"},\"router\":\"sysinfo\",\"data\":{}}";
        if (targetNeClass.equals("linux")) {
            cmd = "/simotoken###data###{\"request\":{\"subRequest\":\"hostInfoCheck\",\"mainRequest\":\"systemInfo\",\"ssubRequest\":\"\"},\"router\":\"systemInfo\",\"data\":{}}";
        }
        Object obj = null;
        try {
            obj = execCmd(cmd);
        } catch (SimoQueryException | SimoConnectionException e) {
            return false;
        }
        if (obj == null) {
            return false;
        }
        JSONArray arr = (JSONArray) obj;
        if (arr.size() == 0 || arr.getJSONObject(0).getString("sys_name") == null) {
            return false;
        }

        return true;
    }

    private boolean handShake(CDPSTarget target) {
        Map<String, String> user = new HashMap<>();
        user.put("acc", target.userAcc);
        user.put("name", target.userName);
        user.put("email", target.userEmail);
        user.put("concat", target.userConcat);

        Token token = Token.of();
        token.setData(user);
        tokenStr = token.getToken();
        String json = JSON.toJSONString(token);
        String content;
        try {
            content = encodeData(json);
        } catch (Exception e1) {
            logger.error("error encoding content", e1);
            return false;
        }

        String hand = execPost(urlBase + "/simotoken?data=" + content, null);

        if (hand == null) {
            logger.error("error say hello with CDPS, did not get response");
            return false;
        }

        JSONObject jsonObj = JSON.parseObject(hand);
        if ("success".equals(jsonObj.getString("rstcode"))) {
            return true;
        } else {
            logger.error("error say hello with CDPS, msg ", hand);
            return false;
        }
    }

    private String execPost(String url, String entityJson) {
        HttpPost post = new HttpPost(url);
        post.setConfig(requestConfig);
        post.setHeader("Content-type", "application/json");

        if (entityJson != null) {
            StringEntity entity = new StringEntity(entityJson, ContentType.APPLICATION_JSON);
            if (entity != null) {
                post.setEntity(entity);
            }
        }

        HttpResponse response = null;
        try {
            response = client.execute(post, context);
        } catch (IOException e) {
            logger.error("error in post", e);
            return null;
        }
        if (response != null) {
            String responseStr = readResponse(response);
            return responseStr;
        }
        return null;
    }

    private String readResponse(HttpResponse response) {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        if (response == null) {
            return null;
        }
        if (response.getStatusLine().getStatusCode() >= HttpStatus.SC_BAD_REQUEST) {
            logger.error("bad request", response.getStatusLine().getStatusCode());
            return null;
        }
        HttpEntity entity = response.getEntity();
        InputStream inputStream = null;
        try {
            inputStream = entity.getContent();

            Header header = response.getFirstHeader("Content-Encoding");
            if (header != null && header.getValue().toLowerCase().indexOf("gzip") > -1) {
                inputStream = new GZIPInputStream(inputStream);
            }

            int readBytes = 0;
            byte[] sBuffer = new byte[512];
            while ((readBytes = inputStream.read(sBuffer)) != -1) {
                out.write(sBuffer, 0, readBytes);
                out.flush();
            }
        } catch (Exception e) {
            logger.error("error reading response entity", e);
        }
        String res = null;
        try {
            res = out.toString(DEFAULT_ENCODING);
        } catch (UnsupportedEncodingException e1) {
            logger.error("encoding error", e1);
        }
        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (IOException e) {
                logger.error("error closing inputstream", e);
            }
        }
        try {
            out.close();
        } catch (IOException e) {
            logger.error("error closing outputstream", e);
        }
        return res;
    }

    private String encodeData(String data) {
        RSA rsa = RSA.of().keySize(KEY_SIZE);
        try {
            return Base64.getEncoder().encodeToString(rsa.encode(data.getBytes(), PRIVATE_KEY));
        } catch (Exception e) {
            logger.error("rsa encoding error", e);
            return null;
        }
    }

    @Data
    public static class Token {
        private String token;

        private Map<String, String> data;

        private long time = System.currentTimeMillis();

        private String useOrigin = "Y";

        public static Token of(String token) {
            Token tk = new Token();
            tk.setToken(token);
            return tk;
        }

        public static Token of() {
            Token tk = new Token();
            tk.setToken(UUID.randomUUID().toString());
            return tk;
        }

        public Token put(String key, String value) {
            if (this.data == null) {
                this.data = new HashMap<>();
            }
            this.data.put(key, value);
            return this;
        }
    }

}
